Custom Preset Programming Guide

Custom Presets use a Lua script to define an effect that can be played back on a Matrix. You can use this to create effects that are not available as standard in Designer. Custom Presets are managed using the Media window.

Basics

Custom presets use Lua scripts to define an animation.

For each pixel (x,y) of each frame of that animation, a pixel function is called which returns three numbers, between 0 and 255, which represent the red, green and blue components of the colour of that pixel. Pixel (0,0) is in the top left of the frame, with the positive x axis pointing right and the positive y axis pointing down.

Here is the most simple example of a custom preset:

Listing 1
function pixel(frame,x,y)
    return 255,0,0
end

This fills every pixel of every frame with red. If you do not return all three components of the pixel's colour, the missing components are assumed to be 0, so the following function is equivalent to Listing 1:

Listing 2
function pixel(frame,x,y)
    return 255
end

A real example

To demonstrate what can be achieved with custom presets, we are going to build up a real example as concepts are introduced throughout this guide.

To start, we are going to create a preset that renders a series of vertical red bands:

Listing 3
-- width of the bands in pixels
band_width = 4
-- space between bands in pixels
band_spacing = 1
-- modulo operator (a%b)
function mod(a,b)
    return a - math.floor(a/b)*b
end
-- the pixel function
function pixel(frame,x,y)
    -- use the modulo operator to split the horizontal axis into bands and
    -- decide if we are in the band or in the separator between bands
    if (mod(x,band_width+band_spacing)<band_width) then
        -- in band
        return 255,0,0
    else
        -- in band separator
        return 0,0,0
    end
end

You will note that we have defined a new function, mod, to implement the modulo operator. This was done to make the script more readable. We will discuss user-defined functions again later.

We also defined two variables, band_width and band_spacing. These we placed outside of the pixel function because they are the same for every pixel of every frame of the effect, so it is more efficient to not execute the assignment for every pixel. Any code outside of the pixel function is executed once, before the pixel function is called for the first time.

Animation

Filling every frame of an animation with a single colour is not very exciting, so we can use the frame argument to change the colour of a given pixel (x,y) based on the current frame.

Here is an example:

Listing 4
function pixel(frame,x,y)
    if (x<frame) then
        return 255,0,0
    else
        return 0,0,0
    end
end

This creates a red horizontal wipe, advancing 1 pixel towards the right for each frame. You may have noted that once the wipe reaches the right side of the frame, the whole frame stays red for a period of time before the animation loops back to the beginning. This is because the number of frames exceeded the number of pixels across the frame.

Ideally, we want our effects to loop seamlessly. To do this, we introduce three global variables that have been already been defined for you:

We can rewrite Listing 4 as follows:

Listing 5
function pixel(frame,x,y)
    -- calculate the progress through the animation
    local t = frame/frames
    -- compare the fraction across the effect with the animation progress
    if (x/width<t) then
        return 255,0,0
    else
        return 0,0,0
    end
end

Now, once the red wipe reaches the right side of the frame, it immediately jumps back to the start. Returning to our vertical band example, we are going to introduce animation by changing the height of each band over time:

Listing 6
-- width of the bands in pixels
band_width = 4
-- space between bands in pixels
band_spacing = 1
-- get the combined width of band and separator
local total_band_width = band_width+band_spacing
-- get the number of visible bands
local bands = width/total_band_width
-- modulo operator (a%b)
function mod(a,b)
    return a - math.floor(a/b)*b
end
-- the pixel function
function pixel(frame,x,y)
    if (mod(x,total_band_width)>=band_width) then
        -- in band separator
        return 0,0,0
    end
    -- get the band in which this pixel falls
    local band = math.floor(x/total_band_width)
    -- get the fraction through the effect
    local t = frame/frames
    -- get the height of the band in which this pixel falls
    local band_height = (math.sin((band/bands+t)*math.pi*2)+1)/2
    -- adjust y to be relative to the center of the effect
    y = y-(height/2)+0.5
    -- decide if this pixel is inside the band
    if (math.abs(y)/(height/2) <= band_height) then
        return 255,0,0
    else
        return 0,0,0
    end
end

We are using a sine function to set the height of each band, where the argument to the sine function is offset based on the index of the band and the current fraction through the effect. The result of this is that the height of each band differs from its neighbour according the sine function, and this relationship is modified over time to create a ripple.

More colours than just red

So far, we have just been creating red effects, but there are more colours than red, so why should we stick with that? We will modify the vertical band example to show how different colours can be created. For this example, we introduce the built-in function, hsi_to_rgb, which converts an HSI (hue, saturation, intensity) colour into an RGB (red, green, blue) colour:

Listing 7
-- width of the bands in pixels
band_width = 4
-- space between bands in pixels
band_spacing = 1
-- get the combined width of band and separator
local total_band_width = band_width+band_spacing
-- get the number of visible bands
local bands = width/total_band_width
-- modulo operator (a%b)
function mod(a,b)
    return a - math.floor(a/b)*b
end
-- rainbow lookup
function rainbow(hue)
    return hsi_to_rgb(hue*math.pi*2,1,1)
end
-- the pixel function
function pixel(frame,x,y)
    if (mod(x,total_band_width)>=band_width) then
        -- in band separator
        return 0,0,0
    end
    -- get the band in which this pixel falls
    local band = math.floor(x/total_band_width)
    -- get the fraction through the effect
    local t = frame/frames
    -- get the height of the band in which this pixel falls
    local band_height = (math.sin((band/bands+t)*math.pi*2)+1)/2
    -- adjust y to be relative to the center of the effect
    y = y-(height/2)+0.5
    -- decide if this pixel is inside the band
    local h = math.abs(y)/(height/2)
    if (h <= band_height) then
        return rainbow(band/bands+t)
    else
        -- offset hue by quarter
        return rainbow((band/bands+t)+0.25)
    end
end

We have defined a new function, rainbow, which returns a fully saturated r,g,b value for a given hue. This function is then called with different arguments depending on whether on not a pixel falls inside or outside of a band.

User-defined functions can be used whenever you want to use a similar piece of code in multiple places with differing arguments.

Running this script, you will see that the bands are now coloured with a rainbow which changes over time, and the area above and below the band is filled with a colour that is pi/2 radians out of phase with the band's colour.

Working with colours

Working with colours as 3 separate components can produce a wide variety of effects, but sometimes it is more convenient to treat a colour as a single entity. We can do that with the colour library.

To create a variable of type colour, call colour.new(), passing in three values between 0 and 255 which represent the red, green and blue components of the colour, i.e:

local c = colour.new(255,0,0)

The variable c has the type colour and represents red. Colours have three properties, red, green and blue, which can be used to access and alter that colour. Here is a simple example using the colour type:

Listing 8
function pixel(frame,x,y)
    local c = colour.new(255,0,0)
    return c.red,c.green,c.blue
end

This fills every pixel of every frame with red.

Earlier in this document, we stated that the pixel function should return 3 numbers, representing the red, green and blue components of a colour. This was not the entire truth. We are also allowed to return a single variable of type colour. This function is therefore equivalent to Listing 8:

Listing 9
function pixel(frame,x,y)
    local c = colour.new(255,0,0)
    return c
end

Once again, we return to our vertical band example and use colour variables to specify the band colour and the background colour:

Listing 10
-- width of the bands in pixels
band_width = 4
-- space between bands in pixels
band_spacing = 1
-- the colour of the band
band_colour = colour.new(255,0,0)
-- the colour of the space between bands
background_colour = colour.new(0,0,255)
-- get the combined width of band and separator
local total_band_width = band_width+band_spacing
-- get the number of visible bands
local bands = width/total_band_width
-- modulo operator (a%b)
function mod(a,b)
    return a - math.floor(a/b)*b
end
-- the pixel function
function pixel(frame,x,y)
    if (mod(x,total_band_width)>=band_width) then
        -- in band separator
        return background_colour
    end
    -- get the band in which this pixel falls
    local band = math.floor(x/total_band_width)
    -- get the fraction through the effect
    local t = frame/frames
    -- get the height of the band in which this pixel falls
    local band_height = (math.sin((band/bands+t)*math.pi*2)+1)/4
    -- adjust y to be relative to the center of the effect
    y = y-(height/2)+0.5
    -- decide if this pixel is inside the band
    if (math.abs(y)/height<=band_height) then
        return band_colour
    else
        return background_colour
    end
end

We have added two variables, band_colour (red) and background_colour (blue) and are now returning those values rather than the r,g,b values that we were using previously. You should now see red bands rippling over a blue background.

A simple gradient

The colour library also includes an interpolate function, which takes two colours and a fraction and returns a new colour that is linearly interpolated between the two colours. For example:

Listing 11
local red = colour.new(255,0,0)
local blue = colour.new(0,0,255)
function pixel(frame,x,y)
    -- interpolate between red and blue using the horizontal displacement of x
    -- note that we use (width-1) so the rightmost pixel is completely blue
    return colour.interpolate(red,blue,x/(width-1))
end

This creates a horizontal red to blue gradient. We could have created the same gradient without the colour library as follows:

Listing 12
function pixel(frame,x,y)
    local f = x/(width-1)
    return 255*(1-f),0,(255*f)
end

However, if you changed your mind about the colours that you wanted for your gradient, it would be significantly harder to alter Listing 12 than it would be to change the colours in the first two lines of Listing 11.

Working with gradients

The gradient library adds support for more complicated gradients that cannot be achieved by interpolating between two colours.

To create a new variable of type gradient, call gradient.new(), passing in two colours, i.e:

local c1 = colour.new(255,0,0)
local c2 = colour.new(0,0,255)
local g = gradient.new(c1, c2)

To find the colour of the gradient at a specific point, use the lookup function, passing in a number between 0 and 1. For example:

Listing 13
local red = colour.new(255,0,0)
local blue = colour.new(0,0,255)
local g = gradient.new(red, blue)
function pixel(frame,x,y)
    -- note the use of the colon operator
    return g:lookup(x/(width-1))
end

This creates a horizontal gradient from red to blue, but we have already seen that there are other ways to generate the same result which will probably be more efficient. To show where the gradient library offers more power:

Listing 14
local red = colour.new(255,0,0)
local blue = colour.new(0,0,255)
local g = gradient.new(red,blue)
-- add a third point to the middle of the gradient
local green = colour.new(0,255,0)
g:add_point(0.5,green)
function pixel(frame,x,y)
    return g:lookup(x/(width-1))
end

We used the add_point function to insert a green colour midway between the red and the blue colours. This generates a horizontal gradient that fades from red to green to blue.

Back to the vertical band example, we will use a gradient to colour the bands:

Listing 15
-- width of the bands in pixels
band_width = 4
-- space between bands in pixels
band_spacing = 1
-- the colour of the band
band_gradient = gradient.new(colour.new(255,0,0), colour.new(255,255,0))
-- the colour of the space between bands
background_colour = colour.new(0,0,0)
-- get the combined width of band and separator
local total_band_width = band_width+band_spacing
-- get the number of visible bands
local bands = width/total_band_width
-- modulo operator (a%b)
function mod(a,b)
    return a - math.floor(a/b)*b
end
-- the pixel function
function pixel(frame,x,y)
    if (mod(x,total_band_width)>=band_width) then
        -- in band separator
        return background_colour
    end
    -- get the band in which this pixel falls
    local band = math.floor(x/total_band_width)
    -- get the fraction through the effect
    local t = frame/frames
    -- get the height of the band in which this pixel falls
    local band_height = (math.sin((band/bands+t)*math.pi*2)+1)/2
    -- adjust y to be relative to the center of the effect
    y = y-(height/2)+0.5
    -- decide if this pixel is inside the band
    local h = math.abs(y)/(height/2)
    if (h<=band_height) then
        return band_gradient:lookup(h)
    else
        return background_colour
    end
end

The band_gradient variable is initialised as a red to yellow gradient, and we use band_gradient:lookup(h) to determine the colour of the band at height h.

Working with properties

Custom presets can have properties which will be exposed in Designer whenever the preset is placed on a timeline. This allows a single custom preset to create a wide variety of effects. It also means that you do not have to create near-identical copies of custom presets just to change one parameter, for example, a colour. You can just expose a colour property and specify the desired colour when the preset is placed on a timeline.

To define a property, you would call the function:

property(name, type, default_value, ...)

This must be added to your script outside of any function call.

name is a string and must be unique within a custom preset and must not contain spaces. This name will be used as the name of a global variable that is available in your script, whose value will depend on what has been set for a given instance of your custom preset.

type is the type of the property. It can be one of the following values: BOOLEAN, INTEGER, FLOAT, COLOUR and GRADIENT. This determines what sort of control is presented to the user when placing a custom preset on a timeline.

default_value is the initial value of a property when first added to a timeline. The value passed in here depends on the type of the property, and this is outlined below.

Certain types of properties also allow some addition arguments to be specified, and these will also be described for each type below:

Boolean properties
property("invert", BOOLEAN, true)

The default value should be true or false.

Integer properties
property("count", INTEGER, number, [min], [max], [step])

The default value should be a number between min and max.

min, max and step are optional.

Float properties
property("count", FLOAT, number, [min], [max], [resolution])

The default value should be a number between min and max.

min, max and resolution are optional.

Colour properties
property("background", COLOUR, red, green, blue)

red, green and blue are the default values of the components of the colour.

Gradient properties
property("gradient", GRADIENT, {fraction, red, green, blue}, ...)

The default value of a gradient is a list of fractions and colours, where fraction is in the range [0-1] and specifies where in the gradient the colour is, and red, green and blue is the colour at that position and are in the range [0-255]. You can specify multiple points. For example:

property("gradient", GRADIENT, 0.0, 255, 0, 0, 1.0, 0, 0, 255)

creates a red (255,0,0) point at the start (0.0) and a blue ((0,0,255) point at the end (1.0).

To demonstrate a real example of using properties in scripts:

Listing 16
property("g", GRADIENT, 0.0, 255, 0, 0, 1.0, 0, 0, 255)
function pixel(frame,x,y)
    return g:lookup(x/(width-1))
end

This, by default, creates a horizontal gradient from red to blue, as we saw in Listing 13. However, when this preset is placed on a timeline, there will be a gradient editor available, and you will be able to alter the gradient to be any colour you wish, without having to recompile the script or having to duplicate the custom preset with some small alterations.

We will now modify our vertical band example to expose some properties to make a very versatile effect:

Listing 17
-- width of the bands in pixels
property("band_width", INTEGER, 4, 1)
-- space between bands in pixels
property("band_spacing", INTEGER, 1, 0)
-- the wavelength of the ripple (in terms of current width)
property("wavelength", FLOAT, 1, 0, 16, 2)
-- the direction of the ripple
property("reverse", BOOLEAN, false)
-- the colour of the band
property("band_gradient", GRADIENT, 0, 255, 0, 0, 1, 255, 255, 0)
-- the colour of the space between bands
property("background_colour", COLOUR, 0, 0, 0)
-- get the combined width of band and separator
local total_band_width = band_width+band_spacing
-- get the number of visible bands
local bands = width/total_band_width
-- modulo operator (a%b)
function mod(a,b)
    return a - math.floor(a/b)*b
end
-- the pixel function
function pixel(frame,x,y)
    if (mod(x,total_band_width)>=band_width) then
        -- in band separator
        return background_colour
    end
    -- get the band in which this pixel falls
    local band = math.floor(x/total_band_width)
    -- get the fraction through the effect
    local t = frame/frames
    -- optionally reverse the ripple
    if (reverse) then t = -t end
    -- get the height of the band in which this pixel falls
    local band_height = (math.sin((band/bands/wavelength+t)*math.pi*2)+1)/2
    -- adjust y to be relative to the center of the effect
    y = y-(height/2)+0.5
    -- decide if this pixel is inside the band
    local h = math.abs(y)/(height/2)
    if (h<=band_height) then
        return band_gradient:lookup(h)
    else
        return background_colour
    end
end

You will notice that adding properties to the example involved little more than changing the variable definitions at the start of the script. There are also two new properties, wavelength, for setting the wavelength of the ripple, and reverse, for changing the direction of the ripple.

By adjusting the values of the properties, we can now create a variety of different effects without having to alter the script again.

Colour library summary

colour.new(r,g,b)

Returns a new colour that represents the RGB color specified by the components r, g and b. r, g and b will be limited to the range [0,255].

colour.interpolate(c1,c2,f)

Returns the colour that is linearly interpolated between colour c1 and colour c2 at fraction f. f can fall outside of the range [0,1] and the returned colour will be extrapolated accordingly.

Properties
c:red

The value of the red component [0-255] of colour c.

c:green

The value of the green component [0-255] of colour c.

c:blue

The value of the blue component [0-255] of colour c.

Gradient library summary

gradient.new(c1,c2)

Returns a new gradient with colour c1 at the start and colour c2 at the end.

Functions
g:lookup(f)

Returns the colour at fraction f through the gradient g. f will be limited to the range [0,1].

g:add_point(f, c)

Adds the colour c to the gradient g at fraction f.

Built-in functions

dist(x1,y1,x2,y2)

Returns the distance between coordinate (x1,y1) and coordinate (x2,y2)

dist_from_center(x,y)

Returns the distance between coordinate (x,y) and the center of the frame. This is not the same as calling dist(x,y,width/2,height/2). It takes into account the fact that the center of the frame may fall in the middle of a pixel. For example, if width and height were equal to 5, the center of the frame is the center of the pixel at coordinate (2,2), but calling dist(2,2,width/2,height/2) will return 0.707, which is the distance between the top left of pixel (2,2) and its center. Calling dist_from_center(2,2), where width and height are equal to 5, will return 0.

print(message)

Prints message in the debugger's Output window.

You are advised to remove calls to this function when you have finished debugging because it will allow the script to run faster when used in programming.

rgb_to_hsi(red,green,blue)

Converts an RGB (red, green, blue) colour to an HSI (hue, saturation, intensity) colour. red, green and blue are in the range [0-255]. Returns three numbers, hue is in [0-2PI] radian, saturation and intensity are in the range [0-1].

hsi_to_rgb(hue,saturation,intensity)

Converts an HSI (hue, saturation, intensity) colour into an RGB (red, green, blue) colour. hue is in [0-2PI] radians, saturation and intensity are in the range [0-1]. Returns three numbers in the range [0-255].

Related Topics Link IconRelated Topics